/*
 * Copyright (c) 2007, 2015, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
/*
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.sun.org.apache.regexp.internal;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.File;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.io.StringBufferInputStream;
import java.io.StringReader;
import java.io.IOException;

/**
 * Data driven (and optionally interactive) testing harness to exercise regular
 * expression compiler and matching engine.
 *
 * @author <a href="mailto:jonl@muppetlabs.com">Jonathan Locke</a>
 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
 * @author <a href="mailto:gholam@xtra.co.nz">Michael McCallum</a>
 */
public class RETest {

  // True if we want to see output from success cases
  static final boolean showSuccesses = false;

  // A new line character.
  static final String NEW_LINE = System.getProperty("line.separator");

  // Construct a debug compiler
  REDebugCompiler compiler = new REDebugCompiler();

  /**
   * Main program entrypoint.  If an argument is given, it will be compiled
   * and interactive matching will ensue.  If no argument is given, the
   * file RETest.txt will be used as automated testing input.
   *
   * @param args Command line arguments (optional regular expression)
   */
  public static void main(String[] args) {
    try {
      if (!test(args)) {
        System.exit(1);
      }
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
  }

  /**
   * Testing entrypoint.
   *
   * @param args Command line arguments
   * @throws Exception thrown in case of error
   */
  public static boolean test(String[] args) throws Exception {
    RETest test = new RETest();
    // Run interactive tests against a single regexp
    if (args.length == 2) {
      test.runInteractiveTests(args[1]);
    } else if (args.length == 1) {
      // Run automated tests
      test.runAutomatedTests(args[0]);
    } else {
      System.out.println("Usage: RETest ([-i] [regex]) ([/path/to/testfile.txt])");
      System.out.println("By Default will run automated tests from file 'docs/RETest.txt' ...");
      System.out.println();
      test.runAutomatedTests("docs/RETest.txt");
    }
    return test.failures == 0;
  }

  /**
   * Constructor
   */
  public RETest() {
  }

  /**
   * Compile and test matching against a single expression
   *
   * @param expr Expression to compile and test
   */
  void runInteractiveTests(String expr) {
    RE r = new RE();
    try {
      // Compile expression
      r.setProgram(compiler.compile(expr));

      // Show expression
      say("" + NEW_LINE + "" + expr + "" + NEW_LINE + "");

      // Show program for compiled expression
      PrintWriter writer = new PrintWriter(System.out);
      compiler.dumpProgram(writer);
      writer.flush();

      boolean running = true;
      // Test matching against compiled expression
      while (running) {
        // Read from keyboard
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.print("> ");
        System.out.flush();
        String match = br.readLine();

        if (match != null) {
          // Try a match against the keyboard input
          if (r.match(match)) {
            say("Match successful.");
          } else {
            say("Match failed.");
          }

          // Show subparen registers
          showParens(r);
        } else {
          running = false;
          System.out.println();
        }
      }
    } catch (Exception e) {
      say("Error: " + e.toString());
      e.printStackTrace();
    }
  }

  /**
   * Exit with a fatal error.
   *
   * @param s Last famous words before exiting
   */
  void die(String s) {
    say("FATAL ERROR: " + s);
    System.exit(-1);
  }

  /**
   * Fail with an error. Will print a big failure message to System.out.
   *
   * @param log Output before failure
   * @param s Failure description
   */
  void fail(StringBuffer log, String s) {
    System.out.print(log.toString());
    fail(s);
  }

  /**
   * Fail with an error. Will print a big failure message to System.out.
   *
   * @param s Failure description
   */
  void fail(String s) {
    failures++;
    say("" + NEW_LINE + "");
    say("*******************************************************");
    say("*********************  FAILURE!  **********************");
    say("*******************************************************");
    say("" + NEW_LINE + "");
    say(s);
    say("");
    // make sure the writer gets flushed.
    if (compiler != null) {
      PrintWriter writer = new PrintWriter(System.out);
      compiler.dumpProgram(writer);
      writer.flush();
      say("" + NEW_LINE + "");
    }
  }

  /**
   * Say something to standard out
   *
   * @param s What to say
   */
  void say(String s) {
    System.out.println(s);
  }

  /**
   * Dump parenthesized subexpressions found by a regular expression matcher object
   *
   * @param r Matcher object with results to show
   */
  void showParens(RE r) {
    // Loop through each paren
    for (int i = 0; i < r.getParenCount(); i++) {
      // Show paren register
      say("$" + i + " = " + r.getParen(i));
    }
  }

  /*
   * number in automated test
   */
  int testCount = 0;

  /*
   * Count of failures in automated test
   */
  int failures = 0;

  /**
   * Run automated tests in RETest.txt file (from Perl 4.0 test battery)
   *
   * @throws Exception thrown in case of error
   */
  void runAutomatedTests(String testDocument) throws Exception {
    long ms = System.currentTimeMillis();

    // Some unit tests
    testPrecompiledRE();
    testSplitAndGrep();
    testSubst();
    testOther();

    // Test from script file
    File testInput = new File(testDocument);
    if (!testInput.exists()) {
      throw new Exception("Could not find: " + testDocument);
    }

    BufferedReader br = new BufferedReader(new FileReader(testInput));
    try {
      // While input is available, parse lines
      while (br.ready()) {
        RETestCase testcase = getNextTestCase(br);
        if (testcase != null) {
          testcase.runTest();
        }
      }
    } finally {
      br.close();
    }

    // Show match time
    say(NEW_LINE + NEW_LINE + "Match time = " + (System.currentTimeMillis() - ms) + " ms.");

    // Print final results
    if (failures > 0) {
      say("*************** THERE ARE FAILURES! *******************");
    }
    say("Tests complete.  " + testCount + " tests, " + failures + " failure(s).");
  }

  /**
   * Run automated unit test
   *
   * @throws Exception thrown in case of error
   */
  void testOther() throws Exception {
    // Serialization test 1: Compile regexp and serialize/deserialize it
    RE r = new RE("(a*)b");
    say("Serialized/deserialized (a*)b");
    ByteArrayOutputStream out = new ByteArrayOutputStream(128);
    new ObjectOutputStream(out).writeObject(r);
    ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
    r = (RE) new ObjectInputStream(in).readObject();
    if (!r.match("aaab")) {
      fail("Did not match 'aaab' with deserialized RE.");
    } else {
      say("aaaab = true");
      showParens(r);
    }

    // Serialization test 2: serialize/deserialize used regexp
    out.reset();
    say("Deserialized (a*)b");
    new ObjectOutputStream(out).writeObject(r);
    in = new ByteArrayInputStream(out.toByteArray());
    r = (RE) new ObjectInputStream(in).readObject();
    if (r.getParenCount() != 0) {
      fail("Has parens after deserialization.");
    }
    if (!r.match("aaab")) {
      fail("Did not match 'aaab' with deserialized RE.");
    } else {
      say("aaaab = true");
      showParens(r);
    }

    // Test MATCH_CASEINDEPENDENT
    r = new RE("abc(\\w*)");
    say("MATCH_CASEINDEPENDENT abc(\\w*)");
    r.setMatchFlags(RE.MATCH_CASEINDEPENDENT);
    say("abc(d*)");
    if (!r.match("abcddd")) {
      fail("Did not match 'abcddd'.");
    } else {
      say("abcddd = true");
      showParens(r);
    }

    if (!r.match("aBcDDdd")) {
      fail("Did not match 'aBcDDdd'.");
    } else {
      say("aBcDDdd = true");
      showParens(r);
    }

    if (!r.match("ABCDDDDD")) {
      fail("Did not match 'ABCDDDDD'.");
    } else {
      say("ABCDDDDD = true");
      showParens(r);
    }

    r = new RE("(A*)b\\1");
    r.setMatchFlags(RE.MATCH_CASEINDEPENDENT);
    if (!r.match("AaAaaaBAAAAAA")) {
      fail("Did not match 'AaAaaaBAAAAAA'.");
    } else {
      say("AaAaaaBAAAAAA = true");
      showParens(r);
    }

    r = new RE("[A-Z]*");
    r.setMatchFlags(RE.MATCH_CASEINDEPENDENT);
    if (!r.match("CaBgDe12")) {
      fail("Did not match 'CaBgDe12'.");
    } else {
      say("CaBgDe12 = true");
      showParens(r);
    }

    // Test MATCH_MULTILINE. Test for eol/bol symbols.
    r = new RE("^abc$", RE.MATCH_MULTILINE);
    if (!r.match("\nabc")) {
      fail("\"\\nabc\" doesn't match \"^abc$\"");
    }
    if (!r.match("\rabc")) {
      fail("\"\\rabc\" doesn't match \"^abc$\"");
    }
    if (!r.match("\r\nabc")) {
      fail("\"\\r\\nabc\" doesn't match \"^abc$\"");
    }
    if (!r.match("\u0085abc")) {
      fail("\"\\u0085abc\" doesn't match \"^abc$\"");
    }
    if (!r.match("\u2028abc")) {
      fail("\"\\u2028abc\" doesn't match \"^abc$\"");
    }
    if (!r.match("\u2029abc")) {
      fail("\"\\u2029abc\" doesn't match \"^abc$\"");
    }

    // Test MATCH_MULTILINE. Test that '.' does not matches new line.
    r = new RE("^a.*b$", RE.MATCH_MULTILINE);
    if (r.match("a\nb")) {
      fail("\"a\\nb\" matches \"^a.*b$\"");
    }
    if (r.match("a\rb")) {
      fail("\"a\\rb\" matches \"^a.*b$\"");
    }
    if (r.match("a\r\nb")) {
      fail("\"a\\r\\nb\" matches \"^a.*b$\"");
    }
    if (r.match("a\u0085b")) {
      fail("\"a\\u0085b\" matches \"^a.*b$\"");
    }
    if (r.match("a\u2028b")) {
      fail("\"a\\u2028b\" matches \"^a.*b$\"");
    }
    if (r.match("a\u2029b")) {
      fail("\"a\\u2029b\" matches \"^a.*b$\"");
    }
  }

  private void testPrecompiledRE() {
    // Pre-compiled regular expression "a*b"
    char[] re1Instructions =
        {
            0x007c, 0x0000, 0x001a, 0x007c, 0x0000, 0x000d, 0x0041,
            0x0001, 0x0004, 0x0061, 0x007c, 0x0000, 0x0003, 0x0047,
            0x0000, 0xfff6, 0x007c, 0x0000, 0x0003, 0x004e, 0x0000,
            0x0003, 0x0041, 0x0001, 0x0004, 0x0062, 0x0045, 0x0000,
            0x0000,
        };

    REProgram re1 = new REProgram(re1Instructions);

    // Simple test of pre-compiled regular expressions
    RE r = new RE(re1);
    say("a*b");
    boolean result = r.match("aaab");
    say("aaab = " + result);
    showParens(r);
    if (!result) {
      fail("\"aaab\" doesn't match to precompiled \"a*b\"");
    }

    result = r.match("b");
    say("b = " + result);
    showParens(r);
    if (!result) {
      fail("\"b\" doesn't match to precompiled \"a*b\"");
    }

    result = r.match("c");
    say("c = " + result);
    showParens(r);
    if (result) {
      fail("\"c\" matches to precompiled \"a*b\"");
    }

    result = r.match("ccccaaaaab");
    say("ccccaaaaab = " + result);
    showParens(r);
    if (!result) {
      fail("\"ccccaaaaab\" doesn't match to precompiled \"a*b\"");
    }
  }

  private void testSplitAndGrep() {
    String[] expected = {"xxxx", "xxxx", "yyyy", "zzz"};
    RE r = new RE("a*b");
    String[] s = r.split("xxxxaabxxxxbyyyyaaabzzz");
    for (int i = 0; i < expected.length && i < s.length; i++) {
      assertEquals("Wrong splitted part", expected[i], s[i]);
    }
    assertEquals("Wrong number of splitted parts", expected.length,
        s.length);

    r = new RE("x+");
    expected = new String[]{"xxxx", "xxxx"};
    s = r.grep(s);
    for (int i = 0; i < s.length; i++) {
      say("s[" + i + "] = " + s[i]);
      assertEquals("Grep fails", expected[i], s[i]);
    }
    assertEquals("Wrong number of string found by grep", expected.length,
        s.length);
  }

  private void testSubst() {
    RE r = new RE("a*b");
    String expected = "-foo-garply-wacky-";
    String actual = r.subst("aaaabfooaaabgarplyaaabwackyb", "-");
    assertEquals("Wrong result of substitution in \"a*b\"", expected, actual);

    // Test subst() with backreferences
    r = new RE("http://[\\.\\w\\-\\?/~_@&=%]+");
    actual = r.subst("visit us: http://www.apache.org!",
        "1234<a href=\"$0\">$0</a>", RE.REPLACE_BACKREFERENCES);
    assertEquals("Wrong subst() result",
        "visit us: 1234<a href=\"http://www.apache.org\">http://www.apache.org</a>!", actual);

    // Test subst() with backreferences without leading characters
    // before first backreference
    r = new RE("(.*?)=(.*)");
    actual = r.subst("variable=value",
        "$1_test_$212", RE.REPLACE_BACKREFERENCES);
    assertEquals("Wrong subst() result", "variable_test_value12", actual);

    // Test subst() with NO backreferences
    r = new RE("^a$");
    actual = r.subst("a",
        "b", RE.REPLACE_BACKREFERENCES);
    assertEquals("Wrong subst() result", "b", actual);

    // Test subst() with NO backreferences
    r = new RE("^a$", RE.MATCH_MULTILINE);
    actual = r.subst("\r\na\r\n",
        "b", RE.REPLACE_BACKREFERENCES);
    assertEquals("Wrong subst() result", "\r\nb\r\n", actual);
  }

  public void assertEquals(String message, String expected, String actual) {
    if (expected != null && !expected.equals(actual)
        || actual != null && !actual.equals(expected)) {
      fail(message + " (expected \"" + expected
          + "\", actual \"" + actual + "\")");
    }
  }

  public void assertEquals(String message, int expected, int actual) {
    if (expected != actual) {
      fail(message + " (expected \"" + expected
          + "\", actual \"" + actual + "\")");
    }
  }

  /**
   * Converts yesno string to boolean.
   *
   * @param yesno string representation of expected result
   * @return true if yesno is "YES", false if yesno is "NO" stops program otherwise.
   */
  private boolean getExpectedResult(String yesno) {
    if ("NO".equals(yesno)) {
      return false;
    } else if ("YES".equals(yesno)) {
      return true;
    } else {
      // Bad test script
      die("Test script error!");
      return false; //to please javac
    }
  }

  /**
   * Finds next test description in a given script.
   *
   * @param br <code>BufferedReader</code> for a script file
   * @return strign tag for next test description
   * @throws IOException if some io problems occured
   */
  private String findNextTest(BufferedReader br) throws IOException {
    String number = "";

    while (br.ready()) {
      number = br.readLine();
      if (number == null) {
        break;
      }
      number = number.trim();
      if (number.startsWith("#")) {
        break;
      }
      if (!number.equals("")) {
        say("Script error.  Line = " + number);
        System.exit(-1);
      }
    }
    return number;
  }

  /**
   * Creates testcase for the next test description in the script file.
   *
   * @param br <code>BufferedReader</code> for script file.
   * @return a new tescase or null.
   * @throws IOException if some io problems occured
   */
  private RETestCase getNextTestCase(BufferedReader br) throws IOException {
    // Find next re test case
    final String tag = findNextTest(br);

    // Are we done?
    if (!br.ready()) {
      return null;
    }

    // Get expression
    final String expr = br.readLine();

    // Get test information
    final String matchAgainst = br.readLine();
    final boolean badPattern = "ERR".equals(matchAgainst);
    boolean shouldMatch = false;
    int expectedParenCount = 0;
    String[] expectedParens = null;

    if (!badPattern) {
      shouldMatch = getExpectedResult(br.readLine().trim());
      if (shouldMatch) {
        expectedParenCount = Integer.parseInt(br.readLine().trim());
        expectedParens = new String[expectedParenCount];
        for (int i = 0; i < expectedParenCount; i++) {
          expectedParens[i] = br.readLine();
        }
      }
    }

    return new RETestCase(this, tag, expr, matchAgainst, badPattern,
        shouldMatch, expectedParens);
  }
}

final class RETestCase {

  final private StringBuffer log = new StringBuffer();
  final private int number;
  final private String tag; // number from script file
  final private String pattern;
  final private String toMatch;
  final private boolean badPattern;
  final private boolean shouldMatch;
  final private String[] parens;
  final private RETest test;
  private RE regexp;

  public RETestCase(RETest test, String tag, String pattern,
      String toMatch, boolean badPattern,
      boolean shouldMatch, String[] parens) {
    this.number = ++test.testCount;
    this.test = test;
    this.tag = tag;
    this.pattern = pattern;
    this.toMatch = toMatch;
    this.badPattern = badPattern;
    this.shouldMatch = shouldMatch;
    if (parens != null) {
      this.parens = new String[parens.length];
      for (int i = 0; i < parens.length; i++) {
        this.parens[i] = parens[i];
      }
    } else {
      this.parens = null;
    }
  }

  public void runTest() {
    test.say(tag + "(" + number + "): " + pattern);
    if (testCreation()) {
      testMatch();
    }
  }

  boolean testCreation() {
    try {
      // Compile it
      regexp = new RE();
      regexp.setProgram(test.compiler.compile(pattern));
      // Expression didn't cause an expected error
      if (badPattern) {
        test.fail(log, "Was expected to be an error, but wasn't.");
        return false;
      }

      return true;
    }
    // Some expressions *should* cause exceptions to be thrown
    catch (Exception e) {
      // If it was supposed to be an error, report success and continue
      if (badPattern) {
        log.append("   Match: ERR\n");
        success("Produces an error (" + e.toString() + "), as expected.");
        return false;
      }

      // Wasn't supposed to be an error
      String message = (e.getMessage() == null) ? e.toString() : e.getMessage();
      test.fail(log, "Produces an unexpected exception \"" + message + "\"");
      e.printStackTrace();
    } catch (Error e) {
      // Internal error happened
      test.fail(log, "Compiler threw fatal error \"" + e.getMessage() + "\"");
      e.printStackTrace();
    }

    return false;
  }

  private void testMatch() {
    log.append("   Match against: '" + toMatch + "'\n");
    // Try regular matching
    try {
      // Match against the string
      boolean result = regexp.match(toMatch);
      log.append("   Matched: " + (result ? "YES" : "NO") + "\n");

      // Check result, parens, and iterators
      if (checkResult(result) && (!shouldMatch || checkParens())) {
        // test match(CharacterIterator, int)
        // for every CharacterIterator implementation.
        log.append("   Match using StringCharacterIterator\n");
        if (!tryMatchUsingCI(new StringCharacterIterator(toMatch))) {
          return;
        }

        log.append("   Match using CharacterArrayCharacterIterator\n");
        if (!tryMatchUsingCI(
            new CharacterArrayCharacterIterator(toMatch.toCharArray(), 0, toMatch.length()))) {
          return;
        }

        log.append("   Match using StreamCharacterIterator\n");
        if (!tryMatchUsingCI(new StreamCharacterIterator(new StringBufferInputStream(toMatch)))) {
          return;
        }

        log.append("   Match using ReaderCharacterIterator\n");
        if (!tryMatchUsingCI(new ReaderCharacterIterator(new StringReader(toMatch)))) {
          return;
        }
      }
    }
    // Matcher blew it
    catch (Exception e) {
      test.fail(log, "Matcher threw exception: " + e.toString());
      e.printStackTrace();
    }
    // Internal error
    catch (Error e) {
      test.fail(log, "Matcher threw fatal error \"" + e.getMessage() + "\"");
      e.printStackTrace();
    }
  }

  private boolean checkResult(boolean result) {
    // Write status
    if (result == shouldMatch) {
      success((shouldMatch ? "Matched" : "Did not match")
          + " \"" + toMatch + "\", as expected:");
      return true;
    } else {
      if (shouldMatch) {
        test.fail(log, "Did not match \"" + toMatch + "\", when expected to.");
      } else {
        test.fail(log, "Matched \"" + toMatch + "\", when not expected to.");
      }
      return false;
    }
  }

  private boolean checkParens() {
    // Show subexpression registers
    if (RETest.showSuccesses) {
      test.showParens(regexp);
    }

    log.append("   Paren count: " + regexp.getParenCount() + "\n");
    if (!assertEquals(log, "Wrong number of parens", parens.length, regexp.getParenCount())) {
      return false;
    }

    // Check registers against expected contents
    for (int p = 0; p < regexp.getParenCount(); p++) {
      log.append("   Paren " + p + ": " + regexp.getParen(p) + "\n");

      // Compare expected result with actual
      if ("null".equals(parens[p]) && regexp.getParen(p) == null) {
        // Consider "null" in test file equal to null
        continue;
      }
      if (!assertEquals(log, "Wrong register " + p, parens[p], regexp.getParen(p))) {
        return false;
      }
    }

    return true;
  }

  boolean tryMatchUsingCI(CharacterIterator matchAgainst) {
    try {
      boolean result = regexp.match(matchAgainst, 0);
      log.append("   Match: " + (result ? "YES" : "NO") + "\n");
      return checkResult(result) && (!shouldMatch || checkParens());
    }
    // Matcher blew it
    catch (Exception e) {
      test.fail(log, "Matcher threw exception: " + e.toString());
      e.printStackTrace();
    }
    // Internal error
    catch (Error e) {
      test.fail(log, "Matcher threw fatal error \"" + e.getMessage() + "\"");
      e.printStackTrace();
    }
    return false;
  }

  public boolean assertEquals(StringBuffer log, String message, String expected, String actual) {
    if (expected != null && !expected.equals(actual)
        || actual != null && !actual.equals(expected)) {
      test.fail(log, message + " (expected \"" + expected
          + "\", actual \"" + actual + "\")");
      return false;
    }
    return true;
  }

  public boolean assertEquals(StringBuffer log, String message, int expected, int actual) {
    if (expected != actual) {
      test.fail(log, message + " (expected \"" + expected
          + "\", actual \"" + actual + "\")");
      return false;
    }
    return true;
  }

  /**
   * Show a success
   *
   * @param s Success story
   */
  void success(String s) {
    if (RETest.showSuccesses) {
      test.say("" + RETest.NEW_LINE + "-----------------------" + RETest.NEW_LINE + "");
      test.say("Expression #" + (number) + " \"" + pattern + "\" ");
      test.say("Success: " + s);
    }
  }
}
